package marlon.sjf;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author marlonyao<yaolei135@gmail.com>
 * 
 */
public class SJFSession implements Runnable {
	private Socket sock;
	private DataInputStream sockIn;
	private DataOutputStream sockOut;

	public SJFSession(Socket sock) throws IOException {
		this.sock = sock;
		sockIn = new DataInputStream(sock.getInputStream());
		sockOut = new DataOutputStream(sock.getOutputStream());
	}

	public void run() {
		try {
			try {
				Command cmd = readCommand();
				SJFServer.STDERR.println("command: " + cmd);
				initSession(cmd);
				int exitCode = CommandExecutors.execute(cmd);
				sysExit(exitCode);
			} catch (IOException e) {
				throw new SJFException(SJFErrorCode.IO_ERROR);
			}
		} catch (SJFException e) {
			System.err.println(e);
			sysExit(SJFConstants.EXIT_STATUS_SERVER_ERROR);
		} catch (SJFClientException e) {
			e.getCause().printStackTrace();
			sysExit(SJFConstants.EXIT_STATUS_CLIENT_ERROR);
		} finally {
			cleanSock();
		}
	}

	private void sysExit(int statusCode) {
		try {
			sockOut.writeByte(SJFConstants.CHUNK_EXIT);
			sockOut.writeShort(1); // chunk body size, always 1 for exit chunk
			sockOut.writeByte(statusCode);
		} catch (IOException e) {
			// ignore this exception occurred in system exit
			e.printStackTrace(SJFServer.STDERR);
		}
	}

	private void cleanSock() {
		try {
			System.err.flush();
			System.out.flush();
			sockIn.close();
			sockOut.close();
			sock.close();
		} catch (IOException e) {
			e.printStackTrace(SJFServer.STDERR);
		}
	}

	private void initSession(Command cmd) {
		SJFThreadLocals.STDIN.set(new SJFStdinStream(sockIn));
		SJFThreadLocals.STDOUT.set(new SJFStdoutStream(sockOut));
		SJFThreadLocals.STDERR.set(new SJFStdoutStream(sockOut, true));
		// set user current work directory, so file will be created at client
		// directory by default
		System.setProperty("user.dir", cmd.cwd);
	}

	private Command readCommand() throws IOException {
		int type = sockIn.readByte();
		if (type != SJFConstants.CHUNK_COMMAND)
			throw new SJFException(SJFErrorCode.CHUNK_EXPECT_COMMNAD);
		String name = readString(sockIn);
		int argsSize = sockIn.readUnsignedShort();
		String[] args = new String[argsSize];
		for (int i = 0; i < argsSize; i++) {
			args[i] = readString(sockIn);
		}
		String currentDir = readString(sockIn);
		return new Command(name, args, currentDir);
	}

	private static String readString(DataInputStream din) throws IOException {
		int count = din.readUnsignedShort();
		byte[] bytes = new byte[count];
		din.readFully(bytes);
		return new String(bytes, "UTF-8");
	}

	static class SJFStdinStream extends InputStream {
		private DataInputStream sockIn;
		private byte[] buffer = new byte[0];
		private int current = 0;

		public SJFStdinStream(DataInputStream sockIn) {
			this.sockIn = sockIn;
		}

		@Override
		public int read() throws IOException {
			while (current >= buffer.length) {
				fill();
			}
			return buffer[current++];
		}

		private void fill() throws IOException {
			int type = sockIn.readUnsignedByte();
			if (type != SJFConstants.CHUNK_STDIN) {
				throw new SJFException(SJFErrorCode.CHUNK_EXPECT_STDIN);
			}
			int count = sockIn.readUnsignedShort();
			buffer = new byte[count];
			sockIn.readFully(buffer);
			current = 0;
		}

		@Override
		public int read(byte b[], int off, int len) throws IOException {
			if (len == 0)
				return 0;
			while (current >= buffer.length) {
				fill();
			}
			int copyLen = Math.min(buffer.length - current, len);
			System.arraycopy(buffer, current, b, off, copyLen);
			current += copyLen;
			return copyLen;
		}

		@Override
		public int available() throws IOException {
			return buffer.length - current;
		}
	}

	static class SJFStdoutStream extends OutputStream {
		private DataOutputStream sockOut;
		private int chunkType;

		public SJFStdoutStream(DataOutputStream sockOut, boolean isStderr) {
			this.sockOut = sockOut;
			chunkType = isStderr ? SJFConstants.CHUNK_STDERR
					: SJFConstants.CHUNK_STDOUT;
		}

		public SJFStdoutStream(DataOutputStream sockOut) {
			this(sockOut, false);
		}

		@Override
		public void write(byte b[], int off, int len) throws IOException {
			sockOut.writeByte(chunkType);
			sockOut.writeShort(len);
			sockOut.write(b, off, len);
		}

		@Override
		public void write(byte b[]) throws IOException {
			write(b, 0, b.length);
		}

		@Override
		public void write(int b) throws IOException {
			sockOut.writeByte(chunkType);
			sockOut.writeShort(1);
			sockOut.write(b);
		}
	}
}
